Micron Document
πŸŽ–οΈGitΠ―Ρ€Π°πŸŽ–οΈ


Displaying Rendered β€’ View raw β€’ Download

.skills/testing-ci/SKILL.md 5235b8fc51eb5f46d453effb2deec00bcc6abdbb (5235b8fc) Text, 10.68 KB

Skill: Testing and CI Verification

Description
Guidelines and commands for verifying code changes locally and understanding the Meshtastic-Android CI pipeline. Use this to determine which testing matrix is needed based on the change type.

1) Baseline local verification order

Run in a single invocation for routine changes to ensure code formatting, analysis, and basic compilation:

T282828
./gradlew spotlessApply spotlessCheck detekt assembleDebug Tffa657test allTests


β”‚ Why no T383838clean? Incremental builds are safe and significantly faster. Only use T383838clean when debugging
β”‚ stale cache issues.

β”‚ Why T383838test allTests and not just T383838test: In KMP modules, the T383838test task name is ambiguous. Gradle
β”‚ matches both T383838testAndroid and T383838testAndroidHostTest and refuses to run either, silently skipping KMP
β”‚ modules. T383838allTests is the T383838KotlinTestReport lifecycle task registered by the KMP plugin.
β”‚ Conversely, T383838allTests does not cover pure-Android modules (T383838:androidApp, T383838:core:barcode, etc.),
β”‚ which is why both T383838test and T383838allTests are needed.

Note: If testing Compose UI on the JVM (Robolectric), pin tests to T383838@Config(sdk = [34]) to avoid SDK 35 compatibility crashes.

SharedFlow + backgroundScope in T383838runTest

When testing long-lived coroutines (e.g., T383838Flow.collect loops launched in T383838backgroundScope), use T383838runTest(UnconfinedTestDispatcher()) instead of plain T383838runTest:

T282828
T8b949e// ❌ BAD β€” SharedFlow emissions silently never reach collectors
Tf0883e@Test Tff7b72fun Td2a8ff`inbound packet is forwarded`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTest Tb4b4b4{
Te6edf3backgroundScopeTb4b4b4.Te6edf3launch Tb4b4b4{ Te6edf3sutTb4b4b4.Te6edf3startTb4b4b4(Te6edf3backgroundScopeTb4b4b4) Tb4b4b4}
Te6edf3sharedFlowTb4b4b4.Te6edf3emitTb4b4b4(Te6edf3packetTb4b4b4)
T8b949e// assertion fails β€” collector never receives the emission
Tb4b4b4}

T8b949e// βœ… GOOD β€” UnconfinedTestDispatcher eagerly dispatches subscriber resumptions
Tf0883e@Test Tff7b72fun Td2a8ff`inbound packet is forwarded`Tb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3UnconfinedTestDispatcherTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4{
Te6edf3backgroundScopeTb4b4b4.Te6edf3launch Tb4b4b4{ Te6edf3sutTb4b4b4.Te6edf3startTb4b4b4(Te6edf3backgroundScopeTb4b4b4) Tb4b4b4}
Te6edf3sharedFlowTb4b4b4.Te6edf3emitTb4b4b4(Te6edf3packetTb4b4b4)
T8b949e// assertion passes β€” collector receives emission immediately
Tb4b4b4}


Why: T383838backgroundScope uses T383838StandardTestDispatcher by default, which does not eagerly dispatch T383838SharedFlow subscriber resumptions. Even T383838advanceUntilIdle() won't trigger delivery. T383838UnconfinedTestDispatcher() fixes this by dispatching eagerly. This affects any test where a coroutine in T383838backgroundScope collects from a T383838SharedFlow or T383838MutableSharedFlow.

2) Change-type verification matrix

β€’ T383838docs-only changes: Usually no Gradle run required, but run T383838spotlessCheck if practical.
β€’ T383838UI text/resource changes: T383838spotlessCheck, T383838detekt, T383838assembleDebug.
β€’ T383838feature/commonMain logic changes: T383838spotlessCheck, T383838detekt, T383838test allTests, T383838assembleDebug.
β€’ T383838navigation/DI wiring changes: T383838spotlessCheck, T383838detekt, T383838assembleDebug, T383838test allTests, plus flavor unit tests if available.
β€’ If touching any KMP module, also run T383838kmpSmokeCompile.
β€’ T383838worker/service/background changes: Broad tests, targeted WorkManager checks.
β€’ T383838BLE/networking/core repository: T383838spotlessCheck, T383838detekt, T383838assembleDebug, T383838test allTests.

3) Flavor checks

Run these when relevant to map, provider, or flavor-specific behavior:

T282828
./gradlew lintFdroidDebug lintGoogleDebug
./gradlew testFdroidDebug testGoogleDebug

3b) Screenshot testing (two modules)

Compose Preview Screenshot Testing (AGP/layoutlib) is split into two modules β€” keep the distinction:

β€’ T383838:screenshot-tests β€” visual-regression gate. CI runs T383838:screenshot-tests:validateDebugScreenshotTest. Holds atomic, dual-purpose components. Touching one of these previews is expected to move a gated baseline.
β€’ T383838:docs-screenshots β€” generate-only, NOT validated in CI. Holds doc-framed compositions (crops/full screens tuned for the docs site). Reframe these freely; it never churns the regression gate.

T282828
./gradlew :screenshot-tests:updateDebugScreenshotTest T8b949e# regression goldens
./gradlew :docs-screenshots:updateDebugScreenshotTest T8b949e# doc-framed composition images
./gradlew :screenshot-tests:copyDocsScreenshots T8b949e# copy doc images from BOTH modules β†’ docs/assets


Rendering is host-deterministic (layoutlib): a local T383838update produces references byte-identical to CI, so locally-recorded goldens pass T383838validate. T383838copyDocsScreenshots overwrites a stale committed T383838nodes_detail_local.png each run β€” T383838git checkout it. Public previews consumed cross-module by a wrapper need a T383838detekt-baseline.xml entry (PreviewPublic). New screenshot? Pick the module by purpose; see T383838docs/assets/screenshots/README.md.

3c) Fresh-install manual/agent testing: skip onboarding

Debug builds accept an intent extra to skip the intro flow (T383838MainActivity.kt, T383838BuildConfig.DEBUG-gated β€” never reaches release/Play builds). Pair with T383838pm grant (native Android, no app code) to pre-accept runtime permissions:

T282828
adb shell pm grant <pkg> android.permission.BLUETOOTH_SCAN
adb shell pm grant <pkg> android.permission.BLUETOOTH_CONNECT
adb shell pm grant <pkg> android.permission.ACCESS_FINE_LOCATION
adb shell pm grant <pkg> android.permission.POST_NOTIFICATIONS T8b949e# API 33+
adb shell am start -n <pkg>/org.meshtastic.app.MainActivity --ez skip_onboarding Tffa657true


Use this whenever driving the app from a fresh install/uninstall (screenshot tests, UI automation, agent-driven exploration) instead of clicking through the intro screens.

4) CI Pipeline Architecture

CI is defined in T383838.github/workflows/reusable-check.yml and structured as parallel job groups:

1. T383838lint-check β€” Runs spotless, detekt, Android lint, and KMP smoke compile in a single Gradle invocation (avoids 3x cold-start overhead). Uses T383838fetch-depth: 0 (full clone) for spotless ratcheting and version code calculation. Produces T383838cache_read_only output and computed T383838version_code for downstream jobs.
2. T383838test-shards β€” A 3-shard matrix that runs unit tests in parallel (depends on T383838lint-check):
β€’ T383838shard-core: T383838allTests for all T383838core:* KMP modules.
β€’ T383838shard-feature: T383838allTests for all T383838feature:* KMP modules.
β€’ T383838shard-app: Explicit test tasks for pure-Android/JVM modules (T383838androidApp, T383838desktopApp, T383838core:barcode).
Each shard generates Kover XML coverage and uploads test results + coverage to Codecov with per-shard flags.
Downstream jobs use T383838fetch-depth: 1 and receive T383838VERSION_CODE from lint-check via env var, enabling shallow clones.
3. T383838android-check β€” Builds APKs for all flavors (depends on T383838lint-check).
4. T383838build-desktop β€” Multi-OS matrix (T383838macos-latest, T383838windows-latest, T383838ubuntu-24.04, T383838ubuntu-24.04-arm) that builds desktop distributions via T383838createDistributable (depends on T383838lint-check).
5. T383838screenshot-check β€” Runs T383838:screenshot-tests:validateDebugScreenshotTest (the visual-regression gate) and uploads a diff report. Note: T383838:docs-screenshots is intentionally NOT validated here (generate-only).

Runner Strategy (Three Tiers)
β€’ T383838ubuntu-24.04-arm β€” Lightweight/utility jobs (status checks, labelers, triage, changelog, release metadata, stale, moderation). Benefits from ARM runners' shorter queue times.
β€’ T383838ubuntu-24.04 β€” Main Gradle-heavy jobs (CI T383838lint-check/T383838test-shards/T383838android-check, release builds, Dokka, publish, dependency-submission). Pin for reproducibility.
β€’ Desktop runners: Multi-OS matrix (T383838macos-latest, T383838windows-latest, T383838ubuntu-24.04, T383838ubuntu-24.04-arm) for the T383838build-desktop job and release packaging.

CI Gradle Properties
T383838gradle.properties is tuned for local dev (8g heap, 4g Kotlin daemon). CI uses T383838.github/ci-gradle.properties, which the T383838gradle-setup composite action copies to T383838~/.gradle/gradle.properties. Key CI overrides:
β€’ T383838org.gradle.daemon=false (single-use runners)
β€’ T383838kotlin.incremental=false (fresh checkouts)
β€’ T383838-Xmx4g Gradle heap, T383838-Xmx2g Kotlin daemon
β€’ VFS watching disabled, workers capped at 4
β€’ T383838org.gradle.isolated-projects=true for better parallelism
β€’ Disables unused Android build features (T383838resvalues, T383838shaders)

CI Conventions
β€’ KMP Smoke Compile: T383838./gradlew kmpSmokeCompile is a lifecycle task (registered in T383838RootConventionPlugin) that auto-discovers all KMP modules and depends on their T383838compileKotlinJvm + T383838compileKotlinIosSimulatorArm64 tasks.
β€’ T383838maxParallelForks CI logic: T383838ProjectExtensions.kt checks T383838project.findProperty("ci") == "true" and uses full available processors in CI (4 forks on std runners) vs. half locally. All CI invocations pass T383838-Pci=true.
β€’ Detekt report formats: Detekt.kt checks T383838project.findProperty("ci") == "true" and disables html, txt, md reports in CI; only xml + sarif are retained for GitHub annotations.
β€’ Robolectric SDK caching: The T383838gradle-setup composite action caches T383838~/.m2/repository/org/robolectric to prevent flaky T383838SocketException on SDK downloads. Cache key is T383838robolectric-{version}-sdk{level} β€” update when bumping version or SDK level.
β€’ T383838mavenLocal() gated: Disabled by default to prevent CI cache poisoning. Pass T383838-PuseMavenLocal for local JitPack testing.
β€’ JUnit parallel execution: Enabled project-wide with classes running sequentially (T383838junit.jupiter.execution.parallel.mode.classes.default=same_thread) to avoid T383838Dispatchers.setMain() races. Cross-module parallelism comes from Gradle forks (T383838maxParallelForks).
β€’ T383838test-retry plugin: Applied to all module types (maxRetries=2, maxFailures=10).
β€’ T383838fail-fast: false: Test sharding does not cancel other shards on failure.
β€’ Explicit Gradle task paths: Prefer T383838app:lintFdroidDebug over shorthand T383838lintDebug in CI.
β€’ Pull request CI: Main-only (T383838.github/workflows/pull-request.yml targets T383838main).
β€’ Merge queue hygiene: T383838merge-queue.yml cancels superseded runs for the same PR (GitHub does not auto-cancel destroyed merge-group runs) and skips the heavy pipeline for docs-only entries (T383838docs/**, T383838*.md). T383838rb-check runs ONLY in the merge queue. T383838main-check.yml passes T383838run_lint: false β€” every main commit is a merge-queue-verified merge commit, so main pushes only rebuild the debug APKs for the snapshot release.
β€’ Cache writes: Trusted on T383838main and merge queue runs; other refs use read-only cache.
β€’ Path filtering: T383838check-changes in T383838pull-request.yml must include module dirs plus build/workflow entrypoints (T383838build-logic/**, T383838gradle/**, T383838.github/workflows/**, T383838gradlew, T383838settings.gradle.kts, etc.).
β€’ AboutLibraries: Runs in T383838offlineMode by default (no GitHub/SPDX API calls). Release builds pass T383838-PaboutLibraries.release=true via Fastlane/Gradle CLI to enable remote license fetching. Do NOT re-gate on T383838CI or T383838GITHUB_TOKEN alone.

Served by rngit 1.5.2 - Generated in 0.05s